using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using DDotNet.Data.BusinessObjects.Core;
using DDotNet.Data.BusinessObjects.Core.Resource;
using DDotNet.Data.BusinessObjects.Validation;

namespace DDotNet.Data.BusinessObjects
{
    /// <summary>
    /// This is the base class from which most business collections
    /// or lists will be derived.
    /// </summary>
    /// <typeparam name="T">Type of the business object being defined.</typeparam>
    /// <typeparam name="C">Type of the child objects contained in the list.</typeparam>
    [SuppressMessage(
        "Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    [Serializable()]
    public abstract class BusinessListBase<T, C> : BindingList<C>,
                                                   IEditableCollection, ICloneable
        where T : BusinessListBase<T, C>
        where C : BusinessBase
    {
        #region Constructors

        protected BusinessListBase()
        {
        }

        #endregion

        #region IsDirty, IsValid

        /// <summary>
        /// Gets a value indicating whether this object's data has been changed.
        /// </summary>
        public bool IsDirty
        {
            get
            {
                // any deletions make us dirty
                if (DeletedList.Count > 0) return true;

                // run through all the child objects
                // and if any are dirty then then
                // collection is dirty
                foreach (C child in this)
                    if (child.IsDirty)
                        return true;
                return false;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this object is currently in
        /// a valid state (has no broken validation rules).
        /// </summary>
        public virtual bool IsValid
        {
            get
            {
                // run through all the child objects
                // and if any are invalid then the
                // collection is invalid
                foreach (C child in this)
                    if (!child.IsValid)
                        return false;
                return true;
            }
        }

        #endregion

        #region Begin/Cancel/ApplyEdit

        /// <summary>
        /// Starts a nested edit on the object.
        /// </summary>
        /// <remarks>
        /// <para>
        /// When this method is called the object takes a snapshot of
        /// its current state (the values of its variables). This snapshot
        /// can be restored by calling <see cref="CancelEdit" />
        /// or committed by calling <see cref="ApplyEdit" />.
        /// </para><para>
        /// This is a nested operation. Each call to BeginEdit adds a new
        /// snapshot of the object's state to a stack. You should ensure that 
        /// for each call to BeginEdit there is a corresponding call to either 
        /// CancelEdit or ApplyEdit to remove that snapshot from the stack.
        /// </para><para>
        /// See Chapters 2 and 3 for details on n-level undo and state stacking.
        /// </para><para>
        /// This method triggers the copying of all child object states.
        /// </para>
        /// </remarks>
        public void BeginEdit()
        {
            if (this.IsChild)
                throw new NotSupportedException(ResourceWrapper.Instance.NoBeginEditChildException);

            CopyState();
        }

        /// <summary>
        /// Cancels the current edit process, restoring the object's state to
        /// its previous values.
        /// </summary>
        /// <remarks>
        /// Calling this method causes the most recently taken snapshot of the 
        /// object's state to be restored. This resets the object's values
        /// to the point of the last <see cref="BeginEdit" />
        /// call.
        /// <para>
        /// This method triggers an undo in all child objects.
        /// </para>
        /// </remarks>
        public void CancelEdit()
        {
            if (this.IsChild)
                throw new NotSupportedException(ResourceWrapper.Instance.NoCancelEditChildException);

            UndoChanges();
        }

        /// <summary>
        /// Commits the current edit process.
        /// </summary>
        /// <remarks>
        /// Calling this method causes the most recently taken snapshot of the 
        /// object's state to be discarded, thus committing any changes made
        /// to the object's state since the last 
        /// <see cref="BeginEdit" /> call.
        /// <para>
        /// This method triggers an <see cref="Core.BusinessBase.ApplyEdit"/>
        ///  in all child objects.
        /// </para>
        /// </remarks>
        public void ApplyEdit()
        {
            if (this.IsChild)
                throw new NotSupportedException(ResourceWrapper.Instance.NoApplyEditChildException);

            AcceptChanges();
        }

        #endregion

        #region N-level undo

        void IUndoableObject.CopyState()
        {
            CopyState();
        }

        void IUndoableObject.UndoChanges()
        {
            UndoChanges();
        }

        void IUndoableObject.AcceptChanges()
        {
            AcceptChanges();
        }

        private void CopyState()
        {
            // we are going a level deeper in editing
            _editLevel += 1;

            // cascade the call to all child objects
            foreach (C child in this)
                child.CopyState();

            // cascade the call to all deleted child objects
            foreach (C child in DeletedList)
                child.CopyState();
        }

        private void UndoChanges()
        {
            C child;

            // we are coming up one edit level
            _editLevel -= 1;
            if (_editLevel < 0) _editLevel = 0;

            // Cancel edit on all current items
            for (int index = Count - 1; index >= 0; index--)
            {
                child = this[index];
                child.UndoChanges();
                // if item is below its point of addition, remove
                if (child.EditLevelAdded > _editLevel)
                    RemoveAt(index);
            }

            // cancel edit on all deleted items
            for (int index = DeletedList.Count - 1; index >= 0; index--)
            {
                child = DeletedList[index];
                child.UndoChanges();
                if (child.EditLevelAdded > _editLevel)
                {
                    // if item is below its point of addition, remove
                    DeletedList.RemoveAt(index);
                }
                else
                {
                    // if item is no longer deleted move back to main list
                    if (!child.IsDeleted) UnDeleteChild(child);
                }
            }
        }

        private void AcceptChanges()
        {
            // we are coming up one edit level
            _editLevel -= 1;
            if (_editLevel < 0) _editLevel = 0;

            // cascade the call to all child objects
            foreach (C child in this)
            {
                child.AcceptChanges();
                // if item is below its point of addition, lower point of addition
                if (child.EditLevelAdded > _editLevel) child.EditLevelAdded = _editLevel;
            }

            // cascade the call to all deleted child objects
            for (int index = DeletedList.Count - 1; index >= 0; index--)
            {
                C child = DeletedList[index];
                child.AcceptChanges();
                // if item is below its point of addition, remove
                if (child.EditLevelAdded > _editLevel)
                    DeletedList.RemoveAt(index);
            }
        }

        #endregion

        #region Delete and Undelete child

        private List<C> _deletedList;

        /// <summary>
        /// A collection containing all child objects marked
        /// for deletion.
        /// </summary>
        [SuppressMessage(
            "Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected List<C> DeletedList
        {
            get
            {
                if (_deletedList == null)
                    _deletedList = new List<C>();
                return _deletedList;
            }
        }

        private void DeleteChild(C child)
        {
            // mark the object as deleted
            child.DeleteChild();
            // and add it to the deleted collection for storage
            DeletedList.Add(child);
        }

        private void UnDeleteChild(C child)
        {
            // we are inserting an _existing_ object so
            // we need to preserve the object's editleveladded value
            // because it will be changed by the normal add process
            int saveLevel = child.EditLevelAdded;
            Add(child);
            child.EditLevelAdded = saveLevel;

            // since the object is no longer deleted, remove it from
            // the deleted collection
            DeletedList.Remove(child);
        }

        /// <summary>
        /// Returns <see langword="true"/> if the internal deleted list
        /// contains the specified child object.
        /// </summary>
        /// <param name="item">Child object to check.</param>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public bool ContainsDeleted(C item)
        {
            return DeletedList.Contains(item);
        }

        #endregion

        #region Insert, Remove, Clear

        /// <summary>
        /// This method is called by a child object when it
        /// wants to be removed from the collection.
        /// </summary>
        /// <param name="child">The child object to remove.</param>
        void IEditableCollection.RemoveChild(BusinessBase child)
        {
            Remove((C) child);
        }

        /// <summary>
        /// Sets the edit level of the child object as it is added.
        /// </summary>
        protected override void InsertItem(int index, C item)
        {
            // when an object is inserted we assume it is
            // a new object and so the edit level when it was
            // added must be set
            item.EditLevelAdded = _editLevel;
            item.SetParent(this);
            base.InsertItem(index, item);
        }

        /// <summary>
        /// Marks the child object for deletion and moves it to
        /// the collection of deleted objects.
        /// </summary>
        protected override void RemoveItem(int index)
        {
            // when an object is 'removed' it is really
            // being deleted, so do the deletion work
            C child = this[index];
            DeleteChild(child);
            child.PropertyChanged -= new PropertyChangedEventHandler(Child_PropertyChanged);
            base.RemoveItem(index);
        }

        /// <summary>
        /// Clears the collection, moving all active
        /// items to the deleted list.
        /// </summary>
        protected override void ClearItems()
        {
            while (base.Count > 0) RemoveItem(0);
            base.ClearItems();
        }

        /// <summary>
        /// Replaces the item at the specified index with
        /// the specified item, first moving the original
        /// item to the deleted list.
        /// </summary>
        /// <param name="index">The zero-based index of the item to replace.</param>
        /// <param name="item">
        /// The new value for the item at the specified index. 
        /// The value can be null for reference types.
        /// </param>
        /// <remarks></remarks>
        protected override void SetItem(int index, C item)
        {
            RemoveItem(index);
            base.SetItem(index, item);
        }

        #endregion

        #region Edit level tracking

        // keep track of how many edit levels we have
        private int _editLevel;

        #endregion

        #region IsChild

        [NotUndoable()] private bool _isChild = false;

        /// <summary>
        /// Indicates whether this collection object is a child object.
        /// </summary>
        /// <returns>True if this is a child object.</returns>
        protected bool IsChild
        {
            get { return _isChild; }
        }

        /// <summary>
        /// Marks the object as being a child object.
        /// </summary>
        /// <remarks>
        /// <para>
        /// By default all business objects are 'parent' objects. This means
        /// that they can be directly retrieved and updated into the database.
        /// </para><para>
        /// We often also need child objects. These are objects which are contained
        /// within other objects. For instance, a parent Invoice object will contain
        /// child LineItem objects.
        /// </para><para>
        /// To create a child object, the MarkAsChild method must be called as the
        /// object is created. Please see Chapter 7 for details on the use of the
        /// MarkAsChild method.
        /// </para>
        /// </remarks>
        protected void MarkAsChild()
        {
            _isChild = true;
        }

        #endregion

        #region ICloneable

        object ICloneable.Clone()
        {
            return GetClone();
        }

        /// <summary>
        /// Creates a clone of the object.
        /// </summary>
        /// <returns>A new object containing the exact data of the original object.</returns>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected virtual object GetClone()
        {
            return ObjectCloner.Clone(this);
        }

        /// <summary>
        /// Creates a clone of the object.
        /// </summary>
        /// <returns>A new object containing the exact data of the original object.</returns>
        public T Clone()
        {
            return (T) GetClone();
        }

        #endregion

        #region Cascade Child events

        private void Child_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            for (int index = 0; index < Count; index++)
            {
                if (ReferenceEquals(this[index], sender))
                {
                    OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index));
                    return;
                }
            }
        }

        #endregion

        #region Serialization Notification

        [OnDeserialized()]
        private void OnDeserializedHandler(StreamingContext context)
        {
            foreach (BusinessBase child in this)
            {
                child.SetParent(this);
                child.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged);
            }
            foreach (BusinessBase child in DeletedList)
                child.SetParent(this);
            OnDeserialized(context);
        }

        /// <summary>
        /// This method is called on a newly deserialized object
        /// after deserialization is complete.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected virtual void OnDeserialized(StreamingContext context)
        {
            // do nothing - this is here so a subclass
            // could override if needed
        }

        #endregion

        #region Data Access

        /// <summary>
        /// Saves the object to the database.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Calling this method starts the save operation, causing the all child
        /// objects to be inserted, updated or deleted within the database based on the
        /// each object's current state.
        /// </para><para>
        /// All this is contingent on <see cref="IsDirty" />. If
        /// this value is <see langword="false"/>, no data operation occurs. 
        /// It is also contingent on <see cref="IsValid" />. If this value is 
        /// <see langword="false"/> an exception will be thrown to 
        /// indicate that the UI attempted to save an invalid object.
        /// </para><para>
        /// It is important to note that this method returns a new version of the
        /// business collection that contains any data updated during the save operation.
        /// You MUST update all object references to use this new version of the
        /// business collection in order to have access to the correct object data.
        /// </para><para>
        /// You can override this method to add your own custom behaviors to the save
        /// operation. For instance, you may add some security checks to make sure
        /// the user can save the object. If all security checks pass, you would then
        /// invoke the base Save method via <c>MyBase.Save()</c>.
        /// </para>
        /// </remarks>
        /// <returns>A new object containing the saved values.</returns>
        public virtual T Save()
        {
            if (this.IsChild)
                throw new NotSupportedException(ResourceWrapper.Instance.NoSaveChildException);

            if (_editLevel > 0)
                throw new ValidationException(ResourceWrapper.Instance.NoSaveEditingException);

            if (!IsValid)
                throw new ValidationException(ResourceWrapper.Instance.NoSaveInvalidException);

            if (IsDirty)
                return (T) DataPortal.Update(this);
            else
                return (T) this;
        }

        /// <summary>
        /// Override this method to load a new business object with default
        /// values from the database.
        /// </summary>
        /// <param name="Criteria">An object containing criteria values.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        protected virtual void DataPortal_Create(object criteria)
        {
            throw new NotSupportedException(ResourceWrapper.Instance.CreateNotSupportedException);
        }

        /// <summary>
        /// Override this method to allow retrieval of an existing business
        /// object based on data in the database.
        /// </summary>
        /// <param name="Criteria">An object containing criteria values to identify the object.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        protected virtual void DataPortal_Fetch(object criteria)
        {
            throw new NotSupportedException(ResourceWrapper.Instance.FetchNotSupportedException);
        }

        /// <summary>
        /// Override this method to allow update of a business
        /// object.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        protected virtual void DataPortal_Update()
        {
            throw new NotSupportedException(ResourceWrapper.Instance.UpdateNotSupportedException);
        }

        /// <summary>
        /// Override this method to allow immediate deletion of a business object.
        /// </summary>
        /// <param name="Criteria">An object containing criteria values to identify the object.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        protected virtual void DataPortal_Delete(object criteria)
        {
            throw new NotSupportedException(ResourceWrapper.Instance.DeleteNotSupportedException);
        }

        /// <summary>
        /// Called by the server-side DataPortal prior to calling the 
        /// requested DataPortal_xyz method.
        /// </summary>
        /// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected virtual void DataPortal_OnDataPortalInvoke(DataPortalEventArgs e)
        {
        }

        /// <summary>
        /// Called by the server-side DataPortal after calling the 
        /// requested DataPortal_xyz method.
        /// </summary>
        /// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected virtual void DataPortal_OnDataPortalInvokeComplete(DataPortalEventArgs e)
        {
        }

        /// <summary>
        /// Called by the server-side DataPortal if an exception
        /// occurs during data access.
        /// </summary>
        /// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
        /// <param name="ex">The Exception thrown during data access.</param>
        [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        protected virtual void DataPortal_OnDataPortalException(DataPortalEventArgs e, Exception ex)
        {
        }

        #endregion
    }
}